---
title: 프로바이더 결합하기
---

들어가기 앞서 먼저 [프로바이더란](/docs/concepts/providers)문서를 먼저 읽어주세요.  
이 가이드는 복수의 프로바이더들(providers) 결합하는 방법에 대해서 알아볼 예정입니다. 

## 프로바이더 결합하기

이전에 간단한 프로바이더를 생성하는 방법을 알아보았습니다. 그런데 현실에서는 
수많은 상황 중에서 하나의 프로바이더가 다른 프로바이더의 상태를 읽어 사용하는 경우가 많이 있습니다.

여러 프로바이더를 결합하기 위해서 [ref] 객체를 사용하여 콜백을 전달하거나 [watch] 메소드를 사용할 수 있습니다.

예를 들어, 아래와 같은 프로바이더가 있다고 고려해 보겠습니다.

```dart
final cityProvider = Provider((ref) => 'London');
```

이제 `cityProvider`를 사용하고 싶은 다른 프로바이더를 만들어 보려고 합니다. 

```dart
final weatherProvider = FutureProvider((ref) async {
  // `ref.watch`를 사용해 다른 프로바이더를 읽어올 수 있습니다. 
  // 그리고 프로바이더를 넘겨줄 수 있습니다. (여기서는 cityProvider가 되겠습니다.)
  final city = ref.watch(cityProvider);

  // `cityProvider` 값을 기반으로한 무언가의 결과를 반환할 수 있습니다. 
  return fetchWeather(city: city);
});
```

이상입니다. 여기서 우리는 다른 프로바이더에 의존하는 한개의 프로바이더를 만드는 방법을 알아보았습니다. 

## FAQ

### 구독중인 값이 시간이 지남에 따라 변경되면 어떻게 되나요?

관찰하거나 구독하고 있는 프로바이더에 의존하여 값이 매번 갱신이 될때 마다 새로운 상태값을 
얻게 됩니다. 예를 들어, [StateNotifierProvider]를 구독하고 있거나 [ProviderContainer.refresh]/[ref.refresh]
에 의해 강제적으로 갱신이 되었다면 물론 구독중인 상태값은 갱신되어 전달될 것입니다. 

[watch]를 사용할 때, Riverpod은 상태값의 변화를 검출하고 _자동적_ 으로 프로바이더를 필요할 때 재 실행합니다.

이것은 상태들을 계산하기 유용할 수 있습니다. 
예를 들어 [StateNotifierProvider]가 할일 목록(a todo-list)의 값을 가진다고 생각해 봅시다.

```dart
class TodoList extends StateNotifier<List<Todo>> {
  TodoList(): super(const []);
}

final todoListProvider = StateNotifierProvider((ref) => TodoList());
```


일반적인 사용방법으로 완료된/완료되지 않은 할일만 표시하도록 UI가 할일 목록을 필터링하도록 하는 것입니다.

이러한 시나리오를 구현하는 쉬운 방법은 다음과 같습니다.

- [StateProvider]를 생성합니다. `filterProvider`는 현재 어떤 필터를 선택하고 있는지에 대한 상태 값을 가집니다.

  ```dart
  enum Filter {
    none,
    completed,
    uncompleted,
  }

  final filterProvider = StateProvider((ref) => Filter.none);
  ```

- 필터 메소드(`filterProvider`)와 할일 목록(`todoListProvider`)을 결합하여 필터링된 할일 목록 값을 제공하는 별도의 프로바이더(`filteredTodoListProvider`)를 만듭니다.

  ```dart
  final filteredTodoListProvider = Provider<List<Todo>>((ref) {
    final filter = ref.watch(filterProvider);
    final todos = ref.watch(todoListProvider);

    switch (filter) {
      case Filter.none:
        return todos;
      case Filter.completed:
        return todos.where((todo) => todo.completed).toList();
      case Filter.uncompleted:
        return todos.where((todo) => !todo.completed).toList();
    }
  });
  ```

그리고, UI에서 `filteredTodoListProvider`를 구독하여 필터링된 할일 목록을 받아 올 수 있습니다. 
필터와 할일 목록이 각각 갱신(변경)될때 마다 자동적으로 UI 업데이트가 이루어 집니다.  

이러한 방법으로 접근한 애플리케이션 샘플은 [Todo List
example](https://github.com/rrousselGit/riverpod/tree/master/examples/todos)에서 확인해 볼 수 있습니다.

:::info
이 행위는 [Provider]에 국한되지 않습니다. 그리고 모든 프로바이더에 적용하여 사용 가능합니다. 

예를 들어, 실시간적으로 값이 변하는 것을 지원하는 검색 기능을 구현하기 위해 [FutureProvider]와 함께 [watch]를 결합하여 사용할 수 있습니다.

For example, you could combine [watch] with [FutureProvider] to implement a search
feature that supports live-configuration changes:

```dart
// 현재 검색 필터 값 입니다. ('')
final searchProvider = StateProvider((ref) => '');

/// 매 시간마다 변경될 수 있는 구성(Configurations)
final configsProvider = StreamProvider<Configuration>(...);

final charactersProvider = FutureProvider<List<Character>>((ref) async {
  final search = ref.watch(searchProvider);
  final configs = await ref.watch(configsProvider.future);
  final response = await dio.get('${configs.host}/characters?search=$search');

  return response.data.map((json) => Character.fromJson(json)).toList();
});
```

이 코드는 `characters` 목록을 서비스로 부터 가져옵니다. 그리고 구성(configurations)이 변하거나 검색 쿼리가 변경되면
자동적으로 다시 `characters` 목록을 가져옵니다. 

:::

### 구독없이 프로바이더를 읽을 수 있나요?

때때로 우린 값이 변경되어도 다시 생성되는 작업 없이 프로바이더의 컨텐츠를 읽기 원합니다. 
Sometimes, we want to read the content of a provider, but without re-creating
the value exposed when the value obtained changes.

예를 들어 인증을 위한 사용자 토큰을 다른 프로바이더로 부터 읽어오는 `Repository` 프로바이더가 있다고 생각해 봅시다.
여기서 우리는 [watch]를 사용하고 사용자 토큰이 변경될 때마다 새로운 `Repository`를 생성할 수 있지만 그렇게 하는 것은 거의 소용이 없습니다.

이 경우에는, [watch]와 유사한 기능을 가지는 [read]를 사용할 수 있습니다. 
그러나, 상태 값이 변경될 때 프로바이더가 노출하는 값을 다시 생성하지 않습니다.

이 경우 일반적으로 생성된 객체에 'ref.read'를 전달합니다.
생성된 객체는 원할 때마다 프로바이더를 읽을 수 있습니다.


```dart
final userTokenProvider = StateProvider<String>((ref) => null);

final repositoryProvider = Provider((ref) => Repository(ref.read));

class Repository {
  Repository(this.read);

  /// `ref.read` 함수입니다.
  final Reader read;

  Future<Catalog> fetchCatalog() async {
    String token = read(userTokenProvider);

    final response = await dio.get('/path', queryParameters: {
      'token': token,
    });

    return Catalog.fromJson(response.data);
  }
}
```

:::note

`ref.read`대신에 `ref` 를 객체에 전달해서 사용하도록 합니다. 

```dart
final repositoryProvider = Provider((ref) => Repository(ref));

class Repository {
  Repository(this.ref);

  final Ref ref;
}
```

그러나 `ref.read`를 전달하면 코드가 약간 덜 장황해지고 객체가 `ref.watch`를 사용하지 않을 것입니다.
다시 말해, `ref`대신 `ref.read`를 전달하면 `ref.watch`는 사용할 수 없다는 말과 동일합니다. 

:::

:::위험 **DON'T** : [read]를 프로바이더 내부에서 호출하지 마세요.

```dart
final myProvider = Provider((ref) {
  // 여기서 'read'를 호출하는 것은 좋지 않습니다.
  final value = ref.read(anotherProvider);
});
```

만약 객체의 원치 않는 재빌드을 방지하기 위해 [read]를 사용한 경우,
[프로바이더 갱신이 너무 자주일어나는데 어떻게 개선하면 좋을까요?](#프로바이더-갱신이-너무-자주일어나는데-어떻게-개선하면-좋을까요)
항목을 참고해 주세요.

:::

### 생성자의 매개변수로 [read]를 전달 받는 객체는 어떻게 테스트 하면 좋은가요?

만약 [구독없이 프로바이더를 읽을 수 있나요?](#구독없이-프로바이더를-읽을-수-있나요)에서 사용한 패턴을 사용한다면,
어떻게 객체를 테스트할지 의문이 들 수 있습니다. 

이 시나리오에서는 raw 객체 대신에 프로바이더를 직접 테스트 하는 것이 좋습니다. 
[ProviderContainer] 클래스를 사용하여 테스트를 진행할 수 있습니다. 

```dart
final repositoryProvider = Provider((ref) => Repository(ref.read));

test('fetches catalog', () async {
  final container = ProviderContainer();
  addTearDown(container.dispose);

  Repository repository = container.read(repositoryProvider);

  await expectLater(
    repository.fetchCatalog(),
    completion(Catalog()),
  );
});
```

### 프로바이더 갱신이 너무 자주일어나는데 어떻게 개선하면 좋을까요? 


객체가 너무 자주 다시 생성되는 경우 프로바이더가 갱신에 무관계한 요소를 수신하고 있는 것입니다.

에들 들어, `Configuration` 객체 뿐만 아니라 `host` 속성을 구독하고 있다고 있다고 가정해 봅시다.
`host`의 속성만 변경되었지만 본래 필요없는 값까지 모두 재평가(계산)하여 전반적인 프로바이더의 갱신을 가져올 것 입니다.

해결 방법으로는 분리된 프로바이더를 만드는 것입니다. 즉, 프로바이더를 속성별로 분리하는 것입니다.
다시 말해, `Configuration`(즉 `host`)에서 필요한 것만 노출하는 별도의 프로바이더를 만드는 것입니다.


**AVOID** 객체 전반적인 관찰:

```dart
final configsProvider = StreamProvider<Configuration>(...);

final productsProvider = FutureProvider<List<Product>>((ref) async {
  // 어떤 구성이 변경되었다고 한다면 productsProvider는 products를 다시 가져옵니다.
  final configs = await ref.watch(configsProvider.future);

  return dio.get('${configs.host}/products');
});
```

**PREFER** 실제 사용하는 속성만 관찰:

```dart
final configsProvider = StreamProvider<Configuration>(...);

/// 현재 host값만 노출하는 프로바이더 
final _hostProvider = FutureProvider<String>((ref) async {
  final config = await ref.watch(configsProvider.future);
  return config.host;
});

final productsProvider = FutureProvider<List<Product>>((ref) async {
  /// 호스트(the host)만 구독. 
  /// 만약 구성들의 변화가 발생한다면, 무의미하게 재평가하지 않습니다.
  final host = await ref.watch(_hostProvider.future);

  return dio.get('$host/products');
});
```

[provider]: ../providers/provider
[stateprovider]: ../providers/state_provider
[futureprovider]: ../providers/future_provider
[statenotifierprovider]: ../providers/state_notifier_provider
[ref]: https://pub.dev/documentation/riverpod/latest/riverpod/Ref-class.html
[watch]: https://pub.dev/documentation/riverpod/latest/riverpod/Ref/watch.html
[read]: https://pub.dev/documentation/riverpod/latest/riverpod/Ref/read.html
[providercontainer.refresh]: https://pub.dev/documentation/riverpod/latest/riverpod/ProviderContainer/refresh.html
[ref.refresh]: https://pub.dev/documentation/flutter_riverpod/latest/flutter_riverpod/WidgetRef/refresh.html
